document.body && document.body.classList.add('analytics-modal-open');
function initializeServerReportBuilder() {
document.querySelectorAll('.report-builder-modal.is-server-rendered').forEach(function(modal) {
if (modal.dataset.reportBuilderReady === '1') return;
modal.dataset.reportBuilderReady = '1';
var canvas = modal.querySelector('[data-report-canvas]');
var templates = modal.querySelector('.report-builder-templates');
var activeCell = null;
var draggingItem = null;
var pageHistory = [];
var builderHistory = [];
var activePageIndex = 0;
var pagination = modal.querySelector('[data-report-pagination]');
function expandAllReportProvinces(root) {
if (!root) return;
root.querySelectorAll('.call-detail-province-row').forEach(function(row) { row.classList.add('is-open'); });
root.querySelectorAll('[data-province-child]').forEach(function(child) {
child.style.setProperty('display', 'table-row', 'important');
});
}
expandAllReportProvinces(modal);
function reportRows() {
return Array.from(canvas ? canvas.querySelectorAll('.report-drop-row') : []);
}
function showReportPage(index) {
var rows = reportRows();
if (!rows.length) {
activePageIndex = 0;
if (pagination) pagination.innerHTML = 'No rows';
return;
}
activePageIndex = Math.max(0, Math.min(index, rows.length - 1));
rows.forEach(function(row, i) { row.classList.toggle('is-active-page', i === activePageIndex); });
activeCell = rows[activePageIndex].querySelector('[data-drop-cell]') || null;
if (!pagination) return;
pagination.innerHTML = rows.map(function(_, i) {
return '';
}).join('');
}
function updateReportPages() {
showReportPage(activePageIndex);
}
function activeReportRow() {
return reportRows()[activePageIndex] || null;
}
function saveActivePageState() {
var row = activeReportRow();
if (!row) return;
var stack = pageHistory[activePageIndex] || [];
stack.push(row.innerHTML);
if (stack.length > 25) stack.shift();
pageHistory[activePageIndex] = stack;
}
function saveBuilderState() {
if (!canvas) return;
builderHistory.push(canvas.innerHTML);
if (builderHistory.length > 40) builderHistory.shift();
}
function undoActivePage() {
if (canvas && builderHistory.length) {
canvas.innerHTML = builderHistory.pop();
pageHistory = [];
activePageIndex = Math.max(0, Math.min(activePageIndex, reportRows().length - 1));
activeCell = null;
updateReportPages();
saveReportDraft(false);
return;
}
var row = activeReportRow();
var stack = pageHistory[activePageIndex] || [];
if (!row || !stack.length) return;
row.innerHTML = stack.pop();
pageHistory[activePageIndex] = stack;
activeCell = row.querySelector('[data-drop-cell]') || null;
updateReportPages();
}
function renderExportCharts(root) {
root.querySelectorAll('.call-sector-chart[data-google-stacked-chart]').forEach(function(panel) {
var payload;
try { payload = JSON.parse(panel.getAttribute('data-google-stacked-chart') || '{}'); } catch (e) { payload = {}; }
var groups = Array.isArray(payload.groups) ? payload.groups : [];
var categories = Array.isArray(payload.categories) ? payload.categories : [];
var matrix = payload.matrix || {};
var colors = Array.isArray(payload.colors) ? payload.colors : [];
if (!groups.length || !categories.length) return;
// Live Google rendering removes the original fallback bar wrapper. Recreate it
// in the export clone before drawing the compact, print-safe SVG.
var target = panel.querySelector('.call-bar-chart-wrap');
if (!target) {
target = document.createElement('div');
target.className = 'call-bar-chart-wrap export-bar-chart-wrap';
var totalRow = panel.querySelector('.call-category-total-row');
panel.insertBefore(target, totalRow || null);
}
var width = 700, height = 370, left = 46, right = 46, top = 42, bottom = 104;
var plotWidth = width - left - right, plotHeight = height - top - bottom;
var totals = groups.map(function(group) { return categories.reduce(function(sum, category) { return sum + Number((matrix[group] || {})[category] || 0); }, 0); });
var maxTotal = Math.max.apply(Math, totals.concat([1]));
var slot = plotWidth / groups.length;
var barWidth = Math.max(2, Math.min(58, slot * 0.6));
var labelFontSize = 8;
var maxLabelWidth = Math.max.apply(Math, groups.map(function(group) { return String(group).length * labelFontSize * 0.6; }).concat([20]));
var labelExtent = Math.ceil(maxLabelWidth * Math.SQRT1_2);
left = Math.max(46, labelExtent + 12);
width = Math.max(700, left + right + groups.length * 26);
plotWidth = width - left - right;
slot = plotWidth / groups.length;
barWidth = Math.max(2, Math.min(58, slot * 0.6));
bottom = 52 + labelExtent;
height = top + 240 + bottom;
plotHeight = 240;
function escSvg(value) { return String(value).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); }
var svg = '';
target.innerHTML = svg;
// Match footer cells to the exact SVG plot slots.
var alignedTotals = panel.querySelector('.call-category-total-row');
if (alignedTotals) {
alignedTotals.style.setProperty('grid-template-columns', (left / width * 100).toFixed(6) + '% repeat(' + groups.length + ', minmax(0, 1fr)) ' + (right / width * 100).toFixed(6) + '%', 'important');
alignedTotals.style.setProperty('padding', '0', 'important');
alignedTotals.style.setProperty('width', '100%', 'important');
alignedTotals.style.setProperty('max-width', 'none', 'important');
alignedTotals.style.setProperty('box-sizing', 'border-box', 'important');
}
target.style.setProperty('display', 'block', 'important');
target.style.setProperty('padding', '0', 'important');
target.style.setProperty('background', '#fff', 'important');
panel.querySelectorAll('.export-chart-legend, .call-region-legend, .google-chart-legend').forEach(function(legend) { legend.remove(); });
var legend = document.createElement('div');
legend.className = 'export-chart-legend';
categories.forEach(function(category, categoryIndex) {
var item = document.createElement('span');
var swatch = document.createElement('i');
swatch.style.background = String(colors[categoryIndex] || '#2563eb');
item.appendChild(swatch);
item.appendChild(document.createTextNode(String(category || '-')));
legend.appendChild(item);
});
panel.insertBefore(legend, target);
});
root.querySelectorAll('.call-combined-pie[data-pie-segments]').forEach(function(pie) {
var segments = [];
try { segments = JSON.parse(pie.getAttribute('data-pie-segments') || '[]'); } catch (e) {}
var angle = -90;
var tinyIndex = 0;
var topPaths = [];
var depthPaths = [];
var labels = [];
segments.forEach(function(segment) {
var sweep = Math.max(0, parseFloat(segment.percent || 0)) * 3.6;
if (sweep <= 0) return;
var end = angle + sweep;
var middle = angle + sweep / 2;
var startRad = angle * Math.PI / 180;
var endRad = end * Math.PI / 180;
var middleRad = middle * Math.PI / 180;
var x1 = 50 + 48 * Math.cos(startRad), y1 = 50 + 48 * Math.sin(startRad);
var x2 = 50 + 48 * Math.cos(endRad), y2 = 50 + 48 * Math.sin(endRad);
var labelRadius = sweep >= 359.9 ? 0 : (sweep < 12 ? (tinyIndex++ % 2 === 0 ? 32 : 41) : (sweep < 25 ? 34 : 25));
var labelX = 50 + labelRadius * Math.cos(middleRad);
var labelY = 50 + labelRadius * Math.sin(middleRad);
var large = sweep > 180 ? 1 : 0;
var color = segment.color || '#2563eb';
var shape = sweep >= 359.9
? ''
: '';
var hex = String(color).replace('#', '');
var darkColor = /^[0-9a-f]{6}$/i.test(hex) ? '#' + [0,2,4].map(function(offset) { return Math.round(parseInt(hex.slice(offset,offset+2),16)*0.65).toString(16).padStart(2,'0'); }).join('') : color;
depthPaths.push(shape.replace(/fill="[^"]*"/, 'fill="' + darkColor + '"'));
topPaths.push(shape);
labels.push('' + Number(segment.percent || 0).toFixed(1) + '%');
angle = end;
});
pie.style.setProperty('background', 'none', 'important');
pie.style.setProperty('box-shadow', 'none', 'important');
pie.style.setProperty('overflow', 'visible', 'important');
pie.innerHTML = '';
var pieCard = pie.closest('.call-combined-pie-card');
if (pieCard) {
pieCard.querySelectorAll('.export-pie-legend').forEach(function(legend) { legend.remove(); });
pieCard.classList.add('export-side-pie');
var pieBody = pieCard.querySelector('.call-combined-pie-body');
if (pieBody) {
pieBody.querySelectorAll('.export-pie-side-column').forEach(function(column) { column.remove(); });
var leftLabels = document.createElement('div'), rightLabels = document.createElement('div');
leftLabels.className = 'export-pie-side-column is-left';
rightLabels.className = 'export-pie-side-column is-right';
var split = Math.ceil(segments.length / 2);
segments.forEach(function(segment,index) {
var label = document.createElement('div');
label.textContent = String(segment.label || '-') + ' (' + Number(segment.count || 0).toLocaleString('en-US') + ' calls)';
label.style.color = String(segment.color || '#222');
(index < split ? leftLabels : rightLabels).appendChild(label);
});
pieBody.insertBefore(leftLabels, pieBody.firstChild);
pieBody.appendChild(rightLabels);
}
}
});
root.querySelectorAll('.call-donut-card').forEach(function(card) {
var donut = card.querySelector('.call-donut');
if (!donut) return;
var color = card.style.getPropertyValue('--donut-color').trim() || donut.getAttribute('data-google-donut-color') || '#2563eb';
var count = Number(donut.getAttribute('data-google-donut-count') || 0);
var total = Math.max(count, Number(donut.getAttribute('data-google-donut-total') || 0));
var pct = total > 0 ? (count / total) * 100 : (parseFloat(card.style.getPropertyValue('--donut-pct')) || 0);
var dash = Math.max(0, Math.min(100, pct));
var safeColor = String(color).replace(/&/g, '&').replace(/"/g, '"');
donut.classList.remove('is-google-chart');
donut.style.cssText = 'display:block!important;width:72px!important;height:72px!important;margin:3px auto!important;background:none!important;visibility:visible!important;opacity:1!important;';
donut.innerHTML = '';
}); root.querySelectorAll('.call-stacked-bar span').forEach(function(segment) {
var match = (segment.getAttribute('style') || '').match(/#[0-9a-fA-F]{3,8}/);
if (match) {
segment.style.setProperty('background', match[0], 'important');
segment.style.setProperty('background-image', 'none', 'important');
}
});
}
function reportHtmlForExport(makeBw) {
var rows = reportRows();
var sourceRows = (rows.length ? rows : []).filter(function(row) { return !!row.querySelector('.report-builder-item'); });
// Older saved drafts can contain builder cards without the newer row wrapper.
// Treat those cards as export rows too, so added content is never discarded.
if (!sourceRows.length && canvas) {
sourceRows = Array.from(canvas.querySelectorAll('.report-builder-item')).map(function(item) {
var row = document.createElement('div');
row.className = 'report-drop-row is-full-row';
row.appendChild(item.cloneNode(true));
return row;
});
}
var body = sourceRows.map(function(row, i) {
var clone = row.cloneNode(true);
clone.querySelectorAll('.is-report-excluded, [data-report-excluded="1"]').forEach(function(excluded) { excluded.remove(); });
clone.querySelectorAll('.report-audit-cell, .report-audit-head, .pie-report-audit, .analytics-full-btn, .analytics-excel-btn, .analytics-image-btn, .analytics-region-excel-proxy, .analytics-modal-full-proxy, .analytics-full-proxy, .call-category-summary-btn, .call-province-exclude-filter, .call-province-exclude-one, .call-expand-all-btn, .report-layout-toggle, .report-text-editor, .drag-handle, .report-builder-item-head').forEach(function(el) { el.remove(); });
clone.querySelectorAll('.call-detail-category-row').forEach(function(groupRow) {
var next = groupRow.nextElementSibling, hasData = false;
while (next && !next.classList.contains('call-detail-category-row') && !next.classList.contains('call-detail-province-row')) {
if (next.classList.contains('call-detail-data-row')) { hasData = true; break; }
next = next.nextElementSibling;
}
if (!hasData) groupRow.remove();
});
clone.querySelectorAll('.call-detail-province-row').forEach(function(groupRow) {
var key = groupRow.getAttribute('data-province-group');
if (key && !clone.querySelector('[data-province-child="' + key + '"].call-detail-data-row')) groupRow.remove();
});
clone.querySelectorAll('.call-detail-province-row').forEach(function(provinceRow) {
provinceRow.classList.add('is-open');
});
clone.querySelectorAll('[data-province-child]').forEach(function(childRow) {
childRow.style.setProperty('display', 'table-row', 'important');
});
clone.querySelectorAll('.call-province-toggle i').forEach(function(icon) { icon.remove(); });
// Export must never reuse the live Google SVG: it carries live-page dimensions
// and is the cause of charts being clipped in the printed A4 document.
clone.querySelectorAll('.has-google-chart').forEach(function(chartElement) {
chartElement.classList.remove('has-google-chart');
chartElement.removeAttribute('data-google-chart-ready');
});
clone.querySelectorAll('.google-stacked-chart-stage').forEach(function(stage) { stage.remove(); });
renderExportCharts(clone);
clone.querySelectorAll('[draggable]').forEach(function(el) { el.removeAttribute('draggable'); });
clone.querySelectorAll('[contenteditable]').forEach(function(el) { el.removeAttribute('contenteditable'); });
// Preserve each Report Builder row as one section, but let native print
// place consecutive sections in the remaining page space.
return '';
}).join('');
var styleLinks = Array.from(document.querySelectorAll('link[rel="stylesheet"]')).map(function(link) {
return '';
}).join('');
var inlineStyles = ''; // Global app styles may contain color-mix()/color(), unsupported by html2canvas.
var css = '';
css += '';
css += '';
css += ''; css += ''; return '
SLIMS Report' + styleLinks + inlineStyles + css + '' + (body || 'No report rows added.
') + '';
}
function downloadReport(filename, mime, content) {
var blob = new Blob([content], { type: mime });
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
setTimeout(function() { URL.revokeObjectURL(url); a.remove(); }, 600);
}
var reportKind = new URLSearchParams(window.location.search).get('analytics_tab') === 'sms' ? 'sms' : 'calls';
var sheetRecordKey = 'slims.' + reportKind + '.report.sheet.records';
function readSheetRecords() {
try { return JSON.parse(localStorage.getItem(sheetRecordKey) || '[]'); } catch (e) { return []; }
}
function writeSheetRecords(records) {
try { localStorage.setItem(sheetRecordKey, JSON.stringify(records.slice(0, 40))); } catch (e) {}
}
function showReportToast(message) {
var toast = modal.querySelector('[data-report-toast]');
if (!toast) return;
toast.textContent = message || 'Saved successfully.';
toast.classList.add('is-visible');
clearTimeout(showReportToast.timer);
showReportToast.timer = setTimeout(function() { toast.classList.remove('is-visible'); }, 2200);
}
function showSavedReportModal() {
var existing = document.querySelector('.report-saved-modal');
if (existing) existing.remove();
var savedModal = document.createElement('div');
savedModal.className = 'report-saved-modal';
savedModal.innerHTML = '
Your file has been saved successfully.
';
document.body.appendChild(savedModal);
savedModal.addEventListener('click', function(event) {
if (event.target === savedModal || event.target.closest('button')) savedModal.remove();
});
}
function saveSheetRecord(format, filename, html) {
if (window.saveSheetRecord) { return window.saveSheetRecord(format, filename, html); }
return Promise.resolve({ok:false});
}
var reportStateParams = new URLSearchParams(window.location.search);
reportStateParams.delete('report_builder');
var reportDraftKey = 'slims.report.builder.draft.v2.' + reportKind + '.' + encodeURIComponent(reportStateParams.toString());
function draftContextKey(query) {
var params = new URLSearchParams(String(query || ''));
['report_builder', 'excluded_call_ids', 'excluded_call_ids[]', 'call_tab', 'analytics_tab'].forEach(function(key) { params.delete(key); });
var keys = ['date_from', 'date_to', 'call_sector_categories[]', 'call_sector_categories', 'excluded_provinces[]', 'excluded_provinces'];
return keys.map(function(key) { return key + '=' + params.getAll(key).map(String).sort().join(','); }).join('&');
}
function parseSavedDraft(key) {
try {
var raw = localStorage.getItem(key);
var draft = raw ? JSON.parse(raw) : null;
return draft && draft.html ? draft : null;
} catch (e) { return null; }
}
function readReportDraft() {
var current = parseSavedDraft(reportDraftKey);
if (current) return current;
// Compatibility: selections may change the URL, but an existing Report Builder
// draft must remain available. Find the latest same-date/category draft and migrate it.
var context = draftContextKey(reportStateParams.toString());
var prefixes = ['slims.report.builder.draft.v2.' + reportKind + '.', 'slims.report.builder.draft.' + reportKind + '.'];
var candidates = [];
try {
for (var i = 0; i < localStorage.length; i++) {
var key = localStorage.key(i) || '';
var prefix = prefixes.find(function(item) { return key.indexOf(item) === 0; });
if (!prefix || key === reportDraftKey) continue;
var savedQuery = '';
try { savedQuery = decodeURIComponent(key.slice(prefix.length)); } catch (ignored) { continue; }
if (draftContextKey(savedQuery) !== context) continue;
var draft = parseSavedDraft(key);
if (draft) candidates.push(draft);
}
} catch (e) {}
candidates.sort(function(a, b) { return String(b.savedAt || '').localeCompare(String(a.savedAt || '')); });
var recovered = candidates[0] || null;
if (recovered) {
try { localStorage.setItem(reportDraftKey, JSON.stringify(recovered)); } catch (e) {}
}
return recovered;
}
function saveReportDraft(showMessage) {
if (!canvas) return;
try {
localStorage.setItem(reportDraftKey, JSON.stringify({html:canvas.innerHTML,activePageIndex:activePageIndex,savedAt:new Date().toISOString()}));
if (showMessage) showReportToast('Draft saved. Continue anytime from Draft.');
} catch (e) { if (showMessage) showReportToast('Draft could not be saved.'); }
}
function refreshDraftAnalyticsData() {
if (!canvas || !templates) return;
var selectors = ['.call-detail-report-panel', '.call-percentage-chart', '.call-category-region-chart', '.call-combined-pie-card', '.call-sector-chart'];
canvas.querySelectorAll('.report-builder-item[data-builder-key]').forEach(function(item) {
var key = item.getAttribute('data-builder-key') || '';
if (!key) return;
var source = templates.querySelector('.report-builder-item[data-builder-key="' + key.replace(/"/g, '') + '"]');
if (!source) return;
selectors.forEach(function(selector) {
var freshParts = Array.from(source.querySelectorAll(selector));
Array.from(item.querySelectorAll(selector)).forEach(function(part, index) {
if (freshParts[index]) part.replaceWith(freshParts[index].cloneNode(true));
});
});
});
}
function restoreReportDraft() {
var draft = readReportDraft();
if (!draft || !draft.html) { showReportToast('No saved draft found.'); return false; }
canvas.innerHTML = draft.html;
// Keep the saved layout, but always carry the latest Analytics included data.
refreshDraftAnalyticsData();
activePageIndex = parseInt(draft.activePageIndex || '0', 10);
pageHistory = []; activeCell = null; updateReportPages();
showReportToast('Draft restored from the last saved point.');
return true;
}
function saveReportOnly() {
saveReportDraft(false);
var generateMenu = modal.querySelector('.report-generate-menu');
if (generateMenu) generateMenu.classList.remove('is-disabled');
showSavedReportModal();
}
function previewReport() {
var existing = document.querySelector('.report-preview-modal');
if (existing) existing.remove();
var previewModal = document.createElement('div');
previewModal.className = 'report-preview-modal';
previewModal.innerHTML = '';
document.body.appendChild(previewModal);
var frame = previewModal.querySelector('.report-preview-frame');
frame.srcdoc = reportHtmlForExport(false);
previewModal.addEventListener('click', function(event) {
if (event.target === previewModal || event.target.closest('.report-preview-close')) previewModal.remove();
});
}
function cleanExportDocument(html) {
var parsed = new DOMParser().parseFromString(html, 'text/html');
parsed.querySelectorAll('link[rel="stylesheet"]').forEach(function(link) { link.remove(); });
return parsed;
}
function exportRenderHost(html) {
var parsed = cleanExportDocument(html);
var host = document.createElement('div');
host.className = 'report-export-render-host';
host.style.cssText = 'position:absolute;left:0;top:0;width:1120px;z-index:2147483646;pointer-events:none;background:#fff;display:block;visibility:visible;overflow:visible;opacity:1;';
host.style.top = (Math.max(document.body.scrollHeight, document.documentElement.scrollHeight) + 100) + 'px';
host.innerHTML = Array.from(parsed.querySelectorAll('style')).map(function(style) {
return '';
}).join('') + parsed.body.innerHTML;
document.body.appendChild(host);
return host;
}
function triggerReportDownload(blob, filename) {
if (!(blob instanceof Blob) || blob.size < 100) throw new Error('Generated file is empty.');
var url = URL.createObjectURL(blob);
var link = document.createElement('a');
link.href = url;
link.download = filename;
link.style.display = 'none';
document.body.appendChild(link);
link.dispatchEvent(new MouseEvent('click', {bubbles:true, cancelable:true, view:window}));
setTimeout(function() { URL.revokeObjectURL(url); link.remove(); }, 15000);
}
function downloadTextPdfFallback(JsPdf, pages, filename) {
var fallback = new JsPdf({orientation:'landscape', unit:'mm', format:'a4', compress:true});
fallback.setFont('helvetica', 'normal');
fallback.setFontSize(10);
var first = true;
pages.forEach(function(page, pageIndex) {
var text = (page.innerText || page.textContent || '').replace(/\s+/g, ' ').trim();
if (!text) return;
var lines = fallback.splitTextToSize(text, 270);
for (var offset = 0; offset < lines.length; offset += 24) {
if (!first) fallback.addPage('a4', 'landscape');
first = false;
fallback.setFontSize(14);
fallback.text('SLIMS Report - Page ' + (pageIndex + 1), 12, 14);
fallback.setFontSize(9);
fallback.text(lines.slice(offset, offset + 24), 12, 24, {maxWidth:270, lineHeightFactor:1.35});
}
});
if (first) throw new Error('No report content is available for PDF.');
triggerReportDownload(fallback.output('blob'), filename);
}
var directPdfLibraries = null;
function ensureDirectPdfLibraries() {
if (typeof window.html2canvas === 'function' && window.jspdf && window.jspdf.jsPDF) {
return Promise.resolve({html2canvas: window.html2canvas, JsPdf: window.jspdf.jsPDF});
}
if (directPdfLibraries) return directPdfLibraries;
var currentScript = Array.from(document.scripts).find(function(script) {
return /analytics-report-builder\.js(?:\?|$)/.test(script.src || '');
});
var baseUrl = currentScript ? currentScript.src : window.location.href;
function loadLibrary(filename, ready) {
if (ready()) return Promise.resolve();
return new Promise(function(resolve, reject) {
var script = document.createElement('script');
script.src = new URL(filename, baseUrl).href;
script.async = true;
script.onload = function() { ready() ? resolve() : reject(new Error(filename + ' did not initialize.')); };
script.onerror = function() { reject(new Error(filename + ' could not be loaded.')); };
document.head.appendChild(script);
});
}
directPdfLibraries = Promise.all([
loadLibrary('html2canvas.min.js', function() { return typeof window.html2canvas === 'function'; }),
loadLibrary('jspdf.umd.min.js', function() { return !!(window.jspdf && window.jspdf.jsPDF); })
]).then(function() {
return {html2canvas: window.html2canvas, JsPdf: window.jspdf.jsPDF};
}).catch(function(error) {
directPdfLibraries = null;
throw error;
});
return directPdfLibraries;
}
function protectedPdfBlocks(page, canvasScale) {
var pageRect = page.getBoundingClientRect();
var selector = '.report-builder-item,.call-sector-chart,.call-percentage-chart,.call-combined-pie-card,.call-category-region-chart';
return Array.from(page.querySelectorAll(selector)).map(function(element) {
var rect = element.getBoundingClientRect();
return {
top: Math.max(0, Math.round((rect.top - pageRect.top) * canvasScale)),
bottom: Math.max(0, Math.round((rect.bottom - pageRect.top) * canvasScale))
};
}).filter(function(block) { return block.bottom > block.top; });
}
function safePdfSliceEnd(startY, idealEnd, canvasHeight, protectedBlocks) {
var endY = Math.min(idealEnd, canvasHeight);
var minimumSlice = Math.max(80, Math.round((idealEnd - startY) * 0.18));
var safeTops = protectedBlocks.filter(function(block) {
return block.top > startY + minimumSlice && block.top < endY && block.bottom > endY;
}).map(function(block) { return block.top; });
if (safeTops.length) endY = Math.max.apply(Math, safeTops);
return endY > startY ? endY : Math.min(idealEnd, canvasHeight);
}
function openNativePdfPrint(html, filename) {
var printWindow = window.open('', '_blank');
if (!printWindow) { showReportToast('Allow pop-ups, then export PDF again.'); return; }
var executiveCss = '';
executiveCss += '';
var printDocument = new DOMParser().parseFromString(html, 'text/html');
printDocument.querySelectorAll('.export-section').forEach(function(section) {
if (!section.querySelector('.call-detail-report-table') || !section.querySelector('.call-sector-chart,.call-percentage-chart,.call-combined-pie-card')) return;
var table = section.querySelector('.call-detail-report-table');
var cell = table.closest('.report-drop-cell') || table.closest('.report-builder-item');
if (cell && !cell.querySelector('.call-sector-chart,.call-percentage-chart,.call-combined-pie-card')) {
var tableSection = printDocument.createElement('section'); tableSection.className = 'export-section';
tableSection.appendChild(cell); section.after(tableSection);
}
});
// Native print cannot paginate long city rowspans reliably.
// Repeat the city on each record in the print copy only.
printDocument.querySelectorAll('.call-detail-report-table tbody td[rowspan]').forEach(function(cell) {
var span = Number(cell.getAttribute('rowspan') || 1);
var column = cell.cellIndex;
var row = cell.parentElement.nextElementSibling;
cell.removeAttribute('rowspan');
cell.classList.remove('call-detail-dist-grouped', 'is-city-group');
for (var remaining = span - 1; remaining > 0 && row; remaining--) {
var copy = cell.cloneNode(true);
copy.classList.remove('is-city-group-start');
row.insertBefore(copy, row.cells[column] || null);
row = row.nextElementSibling;
}
});
executiveCss += '';
// Each province gets its own table with province name in the repeating THEAD.
printDocument.querySelectorAll('.call-detail-report-table').forEach(function(table) {
var sourceBody = table.tBodies[0], sourceHead = table.tHead;
if (!sourceBody || !sourceHead) return;
var panel = table.closest('.call-detail-report-panel');
var titleNode = panel && panel.querySelector('.call-detail-region-title > span');
var fallbackName = titleNode ? titleNode.textContent.trim() : 'Pakistan';
var groups = [], active = null;
Array.from(sourceBody.rows).forEach(function(row) {
if (row.classList.contains('call-detail-province-row')) {
var nameNode = row.querySelector('.call-province-toggle span');
active = {name:nameNode ? nameNode.textContent.trim() : row.textContent.replace(/\d[\d,]*\s*Calls/gi,'').trim(), rows:[]};
groups.push(active); return;
}
if (!active) { active={name:fallbackName,rows:[]};groups.push(active); }
active.rows.push(row);
});
var fragment = printDocument.createDocumentFragment();
groups.forEach(function(group) {
if (!group.rows.some(function(row) {return row.classList.contains('call-detail-data-row');})) return;
var nextTable = table.cloneNode(false);
nextTable.classList.add('print-province-table');
Array.from(table.children).filter(function(child) {return child.tagName==='COLGROUP';}).forEach(function(child){nextTable.appendChild(child.cloneNode(true));});
var head = sourceHead.cloneNode(true), provinceRow = printDocument.createElement('tr'), provinceCell = printDocument.createElement('th');
provinceRow.className='print-province-title';provinceCell.colSpan=Array.from(sourceHead.rows[0].cells).reduce(function(sum,cell){return sum+cell.colSpan;},0);provinceCell.textContent=group.name;
provinceRow.appendChild(provinceCell);head.insertBefore(provinceRow,head.firstChild);nextTable.appendChild(head);
var body = printDocument.createElement('tbody');group.rows.forEach(function(row){body.appendChild(row.cloneNode(true));});nextTable.appendChild(body);fragment.appendChild(nextTable);
});
if (fragment.childNodes.length) table.replaceWith(fragment);
});
executiveCss += '';
// Pair adjacent single pie sections without changing their internal side labels.
var printSections = Array.from(printDocument.querySelectorAll('body > .export-section'));
for (var sectionIndex = 0; sectionIndex < printSections.length - 1; sectionIndex++) {
var first = printSections[sectionIndex], second = printSections[sectionIndex + 1];
if (first.querySelectorAll('.call-combined-pie-card').length === 1 && second.querySelectorAll('.call-combined-pie-card').length === 1 && !first.querySelector('.call-detail-report-table,.call-sector-chart,.call-percentage-chart') && !second.querySelector('.call-detail-report-table,.call-sector-chart,.call-percentage-chart')) {
var pair = printDocument.createElement('div'); pair.className = 'print-pie-pair';
first.before(pair); pair.append(first, second); sectionIndex++;
}
}
// A graph following a completed table starts on a fresh page.
var lastContentWasTable = false;
Array.from(printDocument.body.children).forEach(function(block) {
if (!block.matches('.export-section,.print-pie-pair')) return;
var hasTable = !!block.querySelector('.call-detail-report-table');
var hasGraph = !!block.querySelector('.call-sector-chart,.call-percentage-chart,.call-combined-pie-card,.call-category-region-chart');
if (lastContentWasTable && hasGraph && !hasTable) block.classList.add('print-after-table');
lastContentWasTable = hasTable;
});
executiveCss += '';
html = '' + printDocument.documentElement.outerHTML;
var printableHtml = html.replace(/[\\s\\S]*?<\/title>/i, '').replace('', executiveCss + '');
var ready = false;
printWindow.addEventListener('load', function() {
if (ready) return;
ready = true;
var fonts = printWindow.document.fonts ? printWindow.document.fonts.ready : Promise.resolve();
fonts.then(function() {
return Promise.all(Array.from(printWindow.document.images).map(function(image) {
if (image.complete) return Promise.resolve();
return new Promise(function(resolve) { image.onload = resolve; image.onerror = resolve; });
}));
}).then(function() {
printWindow.requestAnimationFrame(function() {
printWindow.requestAnimationFrame(function() { printWindow.focus(); printWindow.print(); });
});
}).catch(function() { showReportToast('Print preparation failed. Please retry.'); });
}, {once:true});
printWindow.document.open();
printWindow.document.write(printableHtml);
printWindow.document.close();
showReportToast('Choose Save as PDF. Use A4, 100% scale, and turn off Headers and footers. Text and charts stay sharp.');
}
function downloadPdfReport(html, filename) {
showReportToast('Generating PDF for direct download...');
ensureDirectPdfLibraries().then(function(libraries) {
var host = exportRenderHost(html);
var pages = Array.from(host.querySelectorAll('.export-section'));
if (!pages.length) pages = Array.from(host.querySelectorAll('.export-page'));
if (!pages.length) {
host.remove();
throw new Error('Add report content before generating PDF.');
}
// Pack compact report sections into the available A4 space before starting another page.
var flow = document.createElement('div');
flow.className = 'export-pdf-flow';
pages.forEach(function(section) { flow.appendChild(section); });
host.appendChild(flow);
pages = [flow]; var fontsReady = document.fonts && document.fonts.ready ? document.fonts.ready.catch(function(){}) : Promise.resolve();
return fontsReady.then(function() {
var pdf = new libraries.JsPdf({orientation:'portrait', unit:'mm', format:'a4', compress:true});
var pageWidth = 194;
var pageHeight = 281;
var firstPdfPage = true;
var sequence = Promise.resolve();
pages.forEach(function(page) {
sequence = sequence.then(function() {
var bounds = page.getBoundingClientRect();
var cssHeight = Math.ceil(Math.max(bounds.height, page.scrollHeight));
var cssWidth = Math.ceil(Math.max(bounds.width, page.scrollWidth));
if (cssHeight <= 0 || cssWidth <= 0) throw new Error('Report content has no visible size.');
// Stay within browser canvas dimension and memory limits for long reports.
var captureScale = Math.min(2, 16000 / cssHeight, 16000 / cssWidth, Math.sqrt(40000000 / (cssWidth * cssHeight)));
return libraries.html2canvas(page, {
scale: captureScale,
width: cssWidth,
height: cssHeight,
windowWidth: Math.max(1120, cssWidth),
onclone: function(clonedDocument) {
// The live host sits below the page. Move only its capture clone to the origin.
var clonedHost = clonedDocument.querySelector('.report-export-render-host');
if (!clonedHost) throw new Error('Report capture container is missing.');
clonedHost.style.setProperty('top', '0', 'important');
clonedHost.style.setProperty('left', '0', 'important');
clonedHost.style.setProperty('position', 'absolute', 'important');
clonedHost.style.setProperty('visibility', 'visible', 'important');
clonedHost.style.setProperty('opacity', '1', 'important');
clonedDocument.documentElement.style.setProperty('overflow', 'visible', 'important');
clonedDocument.body.style.setProperty('overflow', 'visible', 'important');
},
useCORS: false,
backgroundColor: '#ffffff',
logging: false,
scrollX: 0,
scrollY: 0
}).then(function(canvas) {
if (!canvas.width || !canvas.height) throw new Error('PDF capture is empty. Please try a smaller report.');
var probe = document.createElement('canvas');
probe.width = 100; probe.height = 100;
var probeContext = probe.getContext('2d');
probeContext.drawImage(canvas, 0, 0, 100, 100);
var pixels = probeContext.getImageData(0, 0, 100, 100).data;
var hasContent = false;
for (var pixel = 0; pixel < pixels.length; pixel += 4) {
if (pixels[pixel + 3] > 0 && (pixels[pixel] < 245 || pixels[pixel + 1] < 245 || pixels[pixel + 2] < 245)) { hasContent = true; break; }
}
if (!hasContent) throw new Error('PDF capture was blank. Please refresh the page and try again.');
var canvasScale = canvas.height / cssHeight;
var protectedBlocks = protectedPdfBlocks(page, canvasScale);
var idealSliceHeight = Math.max(1, Math.floor(canvas.width * pageHeight / pageWidth));
var y = 0;
while (y < canvas.height) {
var endY = safePdfSliceEnd(y, y + idealSliceHeight, canvas.height, protectedBlocks);
var currentHeight = Math.max(1, endY - y);
var slice = document.createElement('canvas');
slice.width = canvas.width;
slice.height = currentHeight;
var context = slice.getContext('2d');
context.fillStyle = '#ffffff';
context.fillRect(0, 0, slice.width, slice.height);
context.drawImage(canvas, 0, y, canvas.width, currentHeight, 0, 0, canvas.width, currentHeight);
if (!firstPdfPage) pdf.addPage('a4', 'portrait');
firstPdfPage = false;
var renderedHeight = pageWidth * currentHeight / canvas.width;
pdf.addImage(slice.toDataURL('image/png'), 'PNG', 8, 8, pageWidth, renderedHeight, undefined, 'FAST');
y = endY;
}
});
});
});
return sequence.then(function() {
pdf.save(filename);
host.remove();
showReportToast('PDF downloaded.');
}).catch(function(error) {
host.remove();
throw error;
});
});
}).catch(function(error) {
console.error('SLIMS direct PDF generation failed:', error);
showReportToast(error && error.message ? error.message : 'PDF could not be generated.');
});
}
function wordTableFromChildren(doc, container, columns) {
var children = Array.from(container.children);
var table = doc.createElement('table');
table.setAttribute('cellspacing', '8');
table.setAttribute('cellpadding', '0');
table.style.cssText = 'width:100%;table-layout:fixed;border-collapse:separate;';
for (var i = 0; i < children.length; i += columns) {
var tr = doc.createElement('tr');
for (var j = 0; j < columns; j++) {
var td = doc.createElement('td');
td.style.cssText = 'width:' + (100 / columns) + '%;vertical-align:top;';
if (children[i + j]) td.appendChild(children[i + j]);
tr.appendChild(td);
}
table.appendChild(tr);
}
container.replaceWith(table);
}
function makeWordHtml(html) {
if (typeof window.html2canvas !== 'function') return Promise.resolve(html);
var host = exportRenderHost(html);
var graphics = Array.from(host.querySelectorAll('.call-combined-pie, .call-donut, .call-bar-chart-wrap'));
var chain = Promise.resolve();
graphics.forEach(function(graphic) {
chain = chain.then(function() {
return window.html2canvas(graphic, {scale: 2, useCORS: false, backgroundColor: '#ffffff', logging: false}).then(function(canvas) {
var image = document.createElement('img');
image.src = canvas.toDataURL('image/png');
image.alt = 'Report graph';
image.style.cssText = 'display:block;max-width:100%;height:auto;margin:0 auto;';
graphic.replaceWith(image);
}).catch(function(error) {
console.error('SLIMS Word graph capture failed:', error);
});
});
});
return chain.then(function() {
var doc = cleanExportDocument('' + host.innerHTML + '');
host.remove();
doc.querySelectorAll('.report-drop-row.is-half-row').forEach(function(row) { wordTableFromChildren(doc, row, 2); });
doc.querySelectorAll('.call-combined-pies-grid').forEach(function(grid) { wordTableFromChildren(doc, grid, 2); });
doc.querySelectorAll('.call-donut-grid').forEach(function(grid) { wordTableFromChildren(doc, grid, 4); });
doc.documentElement.setAttribute('xmlns:o', 'urn:schemas-microsoft-com:office:office');
doc.documentElement.setAttribute('xmlns:w', 'urn:schemas-microsoft-com:office:word');
doc.documentElement.setAttribute('xmlns', 'http://www.w3.org/TR/REC-html40');
return '' + doc.documentElement.outerHTML;
});
}
function reportFileDate(value) {
var date = String(value || '').trim();
if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) return date || 'All-Dates';
var parts = date.split('-');
var months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
return parts[2] + '-' + months[Math.max(0, Number(parts[1]) - 1)] + '-' + parts[0];
}
function reportFileScope() {
var params = new URLSearchParams(window.location.search);
var tab = String(params.get('call_tab') || '').toLowerCase();
var names = {kp:'KP',bln:'BLN',gb:'AJK-GB',punjab:'Punjab',sindh:'Sindh',foreign:'Foreign',unknown:'Unknown'};
if (names[tab]) return names[tab];
var heading = modal.querySelector('.analytics-title-block h2, .call-sector-head h2');
var match = heading && heading.textContent.match(/DISTRICT\s+WISE\s*\(([^)]+)\)/i);
return match && match[1] ? match[1].trim().replace(/[^a-z0-9]+/gi, '-') : 'Pakistan';
}
function exportReport(type) {
var html = reportHtmlForExport(false);
var stamp = new Date().toISOString().slice(0, 10);
if (type === 'word') {
var wordName = 'slims-' + reportKind + '-report-' + stamp + '.doc';
showReportToast('Preparing editable Word report...');
makeWordHtml(html).then(function(wordHtml) {
downloadReport(wordName, 'application/msword;charset=utf-8', '' + wordHtml);
showReportToast('Editable Word report downloaded.');
saveSheetRecord('WORD', wordName, wordHtml);
});
return;
}
var currentParams = new URLSearchParams(window.location.search);
var pdfName = reportFileScope() + '-' + reportFileDate(currentParams.get('date_from')) + '-to-' + reportFileDate(currentParams.get('date_to')) + '.pdf';
openNativePdfPrint(html, pdfName);
saveSheetRecord('PDF', pdfName, html);
}
function reportTextNodes(item) {
return Array.from(item.querySelectorAll('.call-combined-pie-row b, .call-region-legend span, .call-bar-item em, .call-donut-card h3')).filter(function(node) { return node.textContent.trim() !== ''; });
}
function setReportLabelText(node, value) {
if (node.matches('.call-region-legend span')) {
var textNode = Array.from(node.childNodes).find(function(child) { return child.nodeType === 3 && child.nodeValue.trim() !== ''; });
if (textNode) { textNode.nodeValue = value; return; }
}
node.textContent = value;
}
function openReportTextEditor(item) {
var existing = item.querySelector('.report-text-editor');
if (existing) { existing.remove(); return; }
var titleNode = item.querySelector('.call-combined-pie-head h2, .analytics-title-block h2, .call-percentage-head h2, .call-category-region-head h2');
var labels = [];
reportTextNodes(item).forEach(function(node) {
var value = node.textContent.trim();
if (value && labels.indexOf(value) === -1) labels.push(value);
});
var panel = document.createElement('div');
panel.className = 'report-text-editor';
panel.innerHTML = 'Labels
';
panel.querySelector('[data-report-title-input]').value = titleNode ? titleNode.textContent.trim() : '';
var list = panel.querySelector('.report-label-editor-list');
labels.forEach(function(label) {
var row = document.createElement('label');
row.innerHTML = '';
row.querySelector('span').textContent = label;
var input = row.querySelector('input');
input.value = label;
input.dataset.originalLabel = label;
list.appendChild(row);
});
if (!labels.length) panel.querySelector('.report-label-editor-title').style.display = 'none';
item.querySelector('.report-builder-item-head').insertAdjacentElement('afterend', panel);
}
function applyReportTextEditor(item, panel) {
var titleNode = item.querySelector('.call-combined-pie-head h2, .analytics-title-block h2, .call-percentage-head h2, .call-category-region-head h2');
var titleInput = panel.querySelector('[data-report-title-input]');
if (titleNode && titleInput && titleInput.value.trim()) titleNode.textContent = titleInput.value.trim();
panel.querySelectorAll('[data-report-label-input]').forEach(function(input) {
var oldValue = input.dataset.originalLabel || '';
var newValue = input.value.trim();
if (!newValue || newValue === oldValue) return;
reportTextNodes(item).forEach(function(node) {
if (node.textContent.trim() === oldValue) setReportLabelText(node, newValue);
});
});
var headTitle = item.querySelector('.report-builder-item-head strong');
if (headTitle && titleInput && titleInput.value.trim()) headTitle.textContent = titleInput.value.trim();
panel.remove();
saveReportDraft(false);
}
function cloneTemplate(key) {
if (!templates || !key) return null;
var source = templates.querySelector('.report-builder-item[data-builder-key="' + key + '"]');
if (!source) return null;
var clone = source.cloneNode(true);
expandAllReportProvinces(clone);
clone.classList.add('is-preview-focus');
setTimeout(function(){ clone.classList.remove('is-preview-focus'); }, 900);
return clone;
}
function addReportRow(type) {
if (!canvas) return null;
saveBuilderState();
var row = document.createElement('div');
row.className = 'report-drop-row ' + (type === 'half' ? 'is-half-row' : 'is-full-row');
row.style.width = '100%';
row.style.maxWidth = 'none';
row.style.justifySelf = 'stretch';
var cells = type === 'half' ? 2 : 1;
for (var i = 0; i < cells; i++) {
var cell = document.createElement('div');
cell.className = 'report-drop-cell';
cell.setAttribute('data-drop-cell', '1');
cell.style.width = '100%';
cell.style.maxWidth = 'none';
row.appendChild(cell);
}
canvas.appendChild(row);
activeCell = row.querySelector('[data-drop-cell]');
activePageIndex = reportRows().length - 1;
updateReportPages();
return row;
}
function seedDefaultReportPages() {
if (!canvas || reportRows().length || readReportDraft()) return;
['server-section-2', 'server-section-3', 'server-section-4'].forEach(function(key) {
var row = addReportRow('full');
var cell = row && row.querySelector('[data-drop-cell]');
var item = cloneTemplate(key);
if (cell && item) cell.appendChild(item);
});
activePageIndex = 0;
updateReportPages();
saveReportDraft(false);
}
seedDefaultReportPages();
modal.querySelectorAll('[data-add-report-row]').forEach(function(btn) {
btn.addEventListener('click', function() { addReportRow(btn.dataset.addReportRow || 'full'); });
});
modal.querySelectorAll('[data-add-report-block]').forEach(function(btn) {
btn.addEventListener('click', function() {
var type = btn.dataset.addReportBlock || 'separator';
var row = addReportRow('full');
if (!row) return;
var cell = row.querySelector('[data-drop-cell]');
var item = document.createElement('section');
item.className = 'report-builder-item report-builder-custom-block';
item.dataset.builderKey = 'custom-' + type + '-' + Date.now();
if (type === 'heading') {
item.innerHTML = 'Type heading
';
} else {
item.innerHTML = '';
}
cell.appendChild(item);
if (type === 'heading') {
var heading = item.querySelector('.report-builder-custom-heading');
heading.focus();
var selection = window.getSelection();
var range = document.createRange();
range.selectNodeContents(heading);
selection.removeAllRanges();
selection.addRange(range);
}
saveReportDraft(false);
});
});
var toolbarUndo = modal.querySelector('[data-undo-report-page]');
if (toolbarUndo) {
toolbarUndo.addEventListener('click', function(event) {
event.preventDefault();
undoActivePage();
});
}
modal.addEventListener('click', function(event) {
var editTextBtn = event.target.closest('[data-edit-report-text]');
if (editTextBtn) {
event.preventDefault();
event.stopPropagation();
var editItem = editTextBtn.closest('.report-builder-item');
if (editItem) openReportTextEditor(editItem);
return;
}
var applyTextBtn = event.target.closest('[data-apply-report-text]');
if (applyTextBtn) {
event.preventDefault();
var editor = applyTextBtn.closest('.report-text-editor');
var editorItem = applyTextBtn.closest('.report-builder-item');
if (editor && editorItem) applyReportTextEditor(editorItem, editor);
return;
}
var cancelTextBtn = event.target.closest('[data-cancel-report-text]');
if (cancelTextBtn) {
event.preventDefault();
var cancelEditor = cancelTextBtn.closest('.report-text-editor');
if (cancelEditor) cancelEditor.remove();
return;
}
var saveDraftBtn = event.target.closest('[data-save-report-draft]');
if (saveDraftBtn) {
event.preventDefault();
saveReportDraft(true);
return;
}
var restoreDraftBtn = event.target.closest('[data-restore-report-draft]');
if (restoreDraftBtn) {
event.preventDefault();
restoreReportDraft();
return;
}
var draftBtn = event.target.closest('[data-report-draft]');
if (draftBtn) {
event.preventDefault();
if (!reportRows().length && readReportDraft()) { restoreReportDraft(); } else { saveReportDraft(true); }
return;
} var previewBtn = event.target.closest('[data-preview-report]');
if (previewBtn) {
event.preventDefault();
event.stopPropagation();
if (event.stopImmediatePropagation) event.stopImmediatePropagation();
previewReport();
return;
}
var saveBtn = event.target.closest('[data-save-report]');
if (saveBtn) {
event.preventDefault();
event.stopPropagation();
if (event.stopImmediatePropagation) event.stopImmediatePropagation();
saveReportOnly();
return;
}
var exportBtn = event.target.closest('[data-export-type]');
if (exportBtn) {
event.preventDefault();
event.stopPropagation();
if (event.stopImmediatePropagation) event.stopImmediatePropagation();
exportReport(exportBtn.dataset.exportType || 'pdf');
}
});
if (pagination) {
pagination.addEventListener('click', function(event) {
var pageBtn = event.target.closest('[data-report-page]');
if (pageBtn) { event.preventDefault(); showReportPage(parseInt(pageBtn.dataset.reportPage || '0', 10)); }
});
}
window.addEventListener('beforeunload', function() { saveReportDraft(false); });
if (canvas && window.MutationObserver) {
var draftTimer = null;
new MutationObserver(function() {
clearTimeout(draftTimer);
draftTimer = setTimeout(function() { saveReportDraft(false); }, 500);
}).observe(canvas, {childList:true,subtree:true,attributes:true,attributeFilter:['class','style']});
}
modal.querySelectorAll('.report-builder-palette-item').forEach(function(btn) {
btn.addEventListener('dragstart', function(event) {
if (!activeCell) addReportRow('full');
event.dataTransfer.setData('text/plain', btn.dataset.builderKey || '');
event.dataTransfer.setData('report-source', 'palette');
event.dataTransfer.effectAllowed = 'copy';
});
btn.addEventListener('click', function() {
if (!activeCell) addReportRow('full');
var clone = cloneTemplate(btn.dataset.builderKey || '');
if (clone && activeCell) {
saveActivePageState();
activeCell.innerHTML = '';
activeCell.appendChild(clone);
updateReportPages();
}
});
});
canvas.addEventListener('click', function(event) {
var expandAll = event.target.closest('.call-expand-all-btn');
if (expandAll) {
event.preventDefault();
event.stopPropagation();
var panel = expandAll.closest('.call-detail-report-panel');
if (!panel) return;
var rows = panel.querySelectorAll('.call-detail-province-row');
var shouldOpen = panel.querySelectorAll('.call-detail-province-row.is-open').length !== rows.length;
rows.forEach(function(row) {
row.classList.toggle('is-open', shouldOpen);
var key = row.getAttribute('data-province-group');
panel.querySelectorAll('[data-province-child="' + key + '"]').forEach(function(child) {
child.style.display = shouldOpen ? '' : 'none';
});
});
expandAll.textContent = shouldOpen ? (expandAll.dataset.collapseLabel || 'Collapse All') : (expandAll.dataset.expandLabel || 'Expand All');
return;
}
var provinceToggle = event.target.closest('.call-province-toggle');
if (provinceToggle) {
event.preventDefault();
event.stopPropagation();
var row = provinceToggle.closest('.call-detail-province-row');
var panel = provinceToggle.closest('.call-detail-report-panel');
if (!row || !panel) return;
var key = row.getAttribute('data-province-group');
var open = row.classList.toggle('is-open');
panel.querySelectorAll('[data-province-child="' + key + '"]').forEach(function(child) {
child.style.display = open ? '' : 'none';
});
var allRows = panel.querySelectorAll('.call-detail-province-row');
var openRows = panel.querySelectorAll('.call-detail-province-row.is-open');
var expandBtn = panel.querySelector('.call-expand-all-btn');
if (expandBtn) expandBtn.textContent = (allRows.length && openRows.length === allRows.length) ? (expandBtn.dataset.collapseLabel || 'Collapse All') : (expandBtn.dataset.expandLabel || 'Expand All');
return;
}
}, true);
canvas.addEventListener('click', function(event) {
var cell = event.target.closest('[data-drop-cell]');
if (cell) {
activeCell = cell;
var row = cell.closest('.report-drop-row');
var rows = reportRows();
var index = rows.indexOf(row);
if (index >= 0 && index !== activePageIndex) showReportPage(index);
}
});
canvas.addEventListener('dragstart', function(event) {
var item = event.target.closest('.report-builder-item');
if (!item) return;
draggingItem = item;
item.classList.add('is-dragging');
event.dataTransfer.setData('text/plain', item.dataset.builderKey || '');
event.dataTransfer.setData('report-source', 'canvas');
event.dataTransfer.effectAllowed = 'move';
});
canvas.addEventListener('dragend', function() {
if (draggingItem) draggingItem.classList.remove('is-dragging');
draggingItem = null;
canvas.querySelectorAll('.is-drop-hover').forEach(function(cell) { cell.classList.remove('is-drop-hover'); });
});
canvas.addEventListener('dragover', function(event) {
var cell = event.target.closest('[data-drop-cell]');
if (!cell) return;
event.preventDefault();
cell.classList.add('is-drop-hover');
}, true);
canvas.addEventListener('dragleave', function(event) {
var cell = event.target.closest('[data-drop-cell]');
if (cell) cell.classList.remove('is-drop-hover');
}, true);
canvas.addEventListener('drop', function(event) {
var cell = event.target.closest('[data-drop-cell]');
if (!cell) return;
event.preventDefault();
event.stopPropagation();
cell.classList.remove('is-drop-hover');
activeCell = cell;
var source = event.dataTransfer.getData('report-source');
var key = event.dataTransfer.getData('text/plain');
var item = source === 'canvas' && draggingItem ? draggingItem : cloneTemplate(key);
if (!item) return;
saveActivePageState();
cell.innerHTML = '';
cell.appendChild(item);
updateReportPages();
saveReportDraft(false);
}, true);
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeServerReportBuilder, {once:true});
} else {
initializeServerReportBuilder();
}